from bs4 import BeautifulSoup as bs
import requests
from pyfzf.pyfzf import FzfPrompt
import os
import re
import sys
import shelve
from tqdm import tqdm

# Не менять, с вероятностю в 100% он не распарсит другие аналогичные сайты.
# Но никто не запрещает попробовать
domain = 'https://yewtu.be'

# Нужен для конструирования ссылки в db_update()
part_link = '/channel/'

# Имя файла БД, при необходимости можно поменять
db_name = 'db.shelve'

# Файл в котором будет храниться история поиска fzf
# Историю поиска можно вызвать сочетаниями Ctrl-p, Ctrl-n назад, вперед
history_file = 'history.txt'

# Файл с сылками на каналы
subscriptions = 'subscriptions.txt'

new_videos_menu = '[Latest Videos]'

fzf = FzfPrompt()

def get_all(first_page_link):
    '''
    Рекурсивно обходит все страницы канала и получает все ссылки и имена видеороликов.
    И дополнительно формирует список сслылок страниц links_pages
    '''
    links_pages = []
    movie_links = []
    movie_names = []
    name_channel = ''
    
    # Цикл по страницам.  
    while first_page_link:        
        soup_page = get_page_soup(first_page_link)
        
        # Формируем список с именами видеороликов
        movie_names.extend(get_names_movies(soup_page))
        # Формируем список с ссылками на видеоролики 
        movie_links.extend(get_links_movies(soup_page))
        
        # Если name_channel пустая занчит это первый проход, поэтому парсим страничку и получаем имя канала. Последующие проходы нам не нужны
        if not name_channel:
            name_channel = get_name_channel(soup_page)

        link = soup_page.find_all('a', href=True, text=re.compile('Next page'))

        # Если есть ссылка на следующую страницу, то присваиваем её переменной first_page_link для следующего цикла обхода.
        # Если ссылки на следующую страницу нет то прерываем цикл.
        if link:
            links_pages.append(link[0].get('href'))
            first_page_link = domain + link[0].get('href')
        else:
            break

    # Ставим цифру в начале имени каждого видео. Нужно для уникальности. Имена видео могут повторятся и тогда будет пропуск в словаре.
    # Потому что в слварях ключи уникальны.
    movie_names = [f'{count}. {movie_name}' for count, movie_name in enumerate(reversed(movie_names), start=1)]

    return dict(zip(movie_names, reversed(movie_links))), name_channel, links_pages

def db_update():
    '''
    Обновляет базу данных. Загружает последнюю страницу каждой подписки и ищет там ссылки на видео которых нет в БД.
    Функционал пока работает только для первой страницы. То есть если человек давно не обновлялся и новые видео 
    выходят за пределы одной страницы (то есть нужно подгружать следующую) то тут это пока не обрабатывается.
    Короче загрузит новые видео только с первой страницы.
    Пока проблему загрузки новых видео с нескольких страниц можно решить... Да бля, пока походу никак. 
    Если удалить подписку из файла, из БД она не удалится... Можно удалить файл БД и запустить скрипт. Он закачает заново все ссылки в БД
    Короче нужно это допилить...
    '''
    flag_upd = False
    with shelve.open(db_name, flag='w', writeback=True) as db:
        for id_channel in tqdm(db['db']['ids_channels'], desc='update db'):
            soup_page = get_page_soup(domain + part_link + id_channel)
            links_movies = get_links_movies(soup_page)
            names_movies = get_names_movies(soup_page)
            movies_data = dict(zip(links_movies, names_movies))

            for link_movie, name_movie in movies_data.items():
                if link_movie not in db['db']['ids_channels'][id_channel]['dictionary_movies'].values():
                    flag_upd = True
                    cnt = len(db['db']['ids_channels'][id_channel]['dictionary_movies']) + 1
                    # Ставим звездочку после номера видео, что бы было понятно что оно новое
                    name_movie = f'{cnt}. {name_movie}'
                    print('This link not in db:', link_movie)
                    print('Name Channel:', db['db']['ids_channels'][id_channel]['name_channel'])
                    print('Name Video:', name_movie)
                    print('Add new movie to db...')
                    db['db']['ids_channels'][id_channel]['dictionary_movies'][name_movie] = link_movie
                    # Добавляем видео в бд новых видео
                    db['db']['new_videos'][link_movie] = (db['db']['ids_channels'][id_channel]['name_channel'], name_movie)
                    
                    print('-'*80)
                    input('Press any key to continue...')
        if not flag_upd:
            print('New videos not found..')
            input('Press any key to continue...')


def get_name_channel(soup_page):
    '''
    Функция возвращает имя канала. На вход нужен soup объект
    '''
    # Получаем имя канала
    return soup_page.select("span")[1].text

def get_names_movies(soup_page):
    '''
    Функция возвращает имена всех видеороликов на странице.
    '''
    clear_movies_names = []
    # Получаем имена видеороликов (грязные данные)
    movie_names = soup_page.find_all("p",  {'dir':'auto'})

    # Очищаем имена видеороликов. В нашем случае каждый 4 элемент это наше имя.
    for movie_name in movie_names[::4]:
        if len(movie_name) == 1:
            clear_movies_names.append(movie_name.text)

    return clear_movies_names

def get_links_movies(link_page):
    '''
    Функция возращает список всех ссылок на видеоролики со страницы.
    '''
    clear_links = []
    # Получаем все ссылки (грязные данные)
    movie_links = link_page.find_all('a', {'style':'width:100%'})
    # Очищаем ссылки, делаем их прямыми)
    for movie_link in movie_links:
        clear_links.append(movie_link.get('href'))
    return clear_links


def db_create():
    '''
    Создает файл базы данных включая основную структуру.
    '''
    with shelve.open(db_name, flag='c', writeback=True) as db:
        db['db'] = {}
        db['db']['ids_channels'] = {}
        db['db']['new_videos'] = {}
    
def db_fill():
    '''
    Функция получает список подписок, затем проверяет нет ли их в бд, если есть то пропускает.
    Если нет то вызывает парсер который возвратит кортеж с данными, которыe добавляется в БД.
    '''
    with shelve.open(db_name, flag='w', writeback=True) as db:        
        for link in tqdm(get_subscriptions(), desc='fill db'):
            id_channel = link.split('/')[2]
            if not id_channel in db['db']['ids_channels']:
                got_all = get_all(domain + link)
                db['db']['ids_channels'][id_channel] = {'dictionary_movies': got_all[0], 'name_channel': got_all[1], 'links_pages': got_all[2]}
    
def db_get():
    '''
    Функция возвращает копию всей БД
    '''
    db_channels = {}
    db_new = {}
    with shelve.open(db_name, flag='r') as db:
        for id_chnl in db['db']['ids_channels']:
            db_channels[id_chnl] = db['db']['ids_channels'][id_chnl]
        db_new = db['db']['new_videos']
        
    return db_channels, db_new

def get_channels_names(db_channels):
    '''
    Функция возвращает словарь ключи которого имена каналов, а значения id каналов
    '''
    names_channels = {}
    for id_chnl in db_channels:
       names_channels[db_channels[id_chnl]['name_channel']] = id_chnl
    return names_channels

def get_subscriptions(subscriptions=subscriptions):
    # Функция возвращает список ваших подписок
    if not os.path.isfile(subscriptions):
        raise Exception(f"File {subscriptions} not found...") 
    list_subs = []
    with open(subscriptions, 'r') as f:
       list_subs = [line.strip() for line in f if len(line.strip()) if not line.startswith('#')]
    return list_subs

def debug_fun():
    # Функция для отладки
    with shelve.open(db_name, flag='c', writeback=True) as db:
        db['db'] = {}
        db['db']['ids_channels'] = {}
        db['db']['new_videos'] = {}

    with shelve.open(db_name, flag='w') as db:
        db['db']['ids_channels']['id_channel'] = {'dfad': 'dkkkkkkkkk'}
        db.sync()

    with shelve.open(db_name, flag='w') as db:
        print(db['db']['ids_channels'])

def get_page_soup(url):
    page = requests.get(url)
    page.encoding = 'utf-8'
    soup_page = bs(page.text, 'html.parser')
    return soup_page

def del_item_new_video(key):
    with shelve.open(db_name, flag='w', writeback=True) as db:
        del db['db']['new_videos'][key]

def main():
# В майне пока полный пиздец, лучше даже не смотрите.
# Я пока не придумал как красиво обрабатывать выходы из fzf с переходами в другие меню и обработкой событий выхода из меню.
# Для полной красоты нехватает только goto

    db_channels = {}
    names_channels = {}
    db_new = {}
    
    if os.path.exists(db_name):
        if input('Do you want to check for new videos? (y/N): ') == 'y':
            db_update()
        print('Check for new subscriptions...')
        db_fill()
        db_channel, db_new = db_get()  
        names_channel = get_channels_names(db_channel)
    elif input('db not found...\nCreate db and fill it? (Y/n):') != 'n':
        db_create()
        db_fill()
        db_channel, db_new = db_get()
        names_channel = get_channels_names(db_channel)
    else:
        sys.exit()

    # Может быть такое что имя меню совпадет с именем какого то канала, теоретически это возможно.
    # Поэтому нужно хоть как то обработать эту ситуацию
    if new_videos_menu in names_channel:
        raise Exception(f'Name {new_videos_menu} exists in the channel list!!!')

    # Подмешиваем в список каналов наше меню новых видео
    names_channel[new_videos_menu] = ''

    while True:

        key_name = fzf.prompt(list(reversed(names_channel)), '--reverse --header="---Список ваших подписок---" --expect=alt-bs')
        if key_name[0] == 'alt-bs':
            break

        # Обработка списка новых видеороликов
        elif key_name[1] == new_videos_menu:
            if len(db_new.items()):
                latest_videos = [f'{key}: {val[0]} --> {val[1]}' for key, val in db_new.items()]
                latest_video_choice = fzf.prompt(reversed(latest_videos), '--reverse -d: --with-nth 2.. --header="---Список последних видео---" --expect=alt-bs,del')
        # Тут обрабатываем выход из меню и удаление нового видео из меню новых видео        
                if latest_video_choice[0] == 'alt-bs': continue
                elif latest_video_choice[0] == 'del':
                    del_item = latest_video_choice[1].split(':', 1)[0]
                    del_item_new_video(del_item)
                    del db_new[del_item]
                    continue

                link_movie = latest_video_choice[1].split(':', 1)[0]
                command = 'mpv ' + 'https://www.youtube.com' + link_movie
                os.system(command)
            continue

        # По имени канала получаем его id
        id_channel = names_channel[key_name[1]]
        names_movies = list(db_channel[id_channel]['dictionary_movies']) 
        names_movies.reverse()
        key_video_name = fzf.prompt(names_movies, f'--reverse --tiebreak=index --header="{key_name[1]}" --cycle --expect=alt-bs --history={history_file}')

        if key_video_name[0] == 'alt-bs': continue

        # Запускаем видеоролик
        link_movie = db_channel[id_channel]['dictionary_movies'][key_video_name[1]]

        command = 'mpv ' + 'https://www.youtube.com' + link_movie
        os.system(command)


if __name__ == "__main__":
    main()
